package memorphic.utils
{
	
	/**
	 * StringTemplate replaces variables in a string with values from an object or Array.
	 * 
	 * For example:
	 * <code>
	 * 		var myTemplate:String = 'hello my name is {name}.'
	 * 		var myData:Object = {name:'Peter'};
	 * 		var myMessage:String = new StringTemplate(myTemplate).apply(myData);
	 * </code>
	 * 
	 * To take values from multiple objects, use LookupChain.
	 */
	public class StringTemplate
	{
		
		public var template:String;
		public var strict:Boolean;
		public var urlEncodeParams:Boolean = false;
		
		private var templateTokens:Array;
		
		
			
		public function get varNames():Array
		{
			return getVarNames();
		}
		
		
		/**
		 * @param template A string containing variable references to be populated when the template is applied to a data object
		 * @param strict If set to true, errors will be thrown if a data value is not found. If false (default) then that part of the template will be left intact
		 */
		public function StringTemplate(template:String, strict:Boolean=false, urlEncodeParams:Boolean=false)
		{
			this.template = template;
			this.strict = strict;
			this.urlEncodeParams = urlEncodeParams;
			templateTokens = new Array();
			parseTemplate(template);
		}
		
		
		
		public static function applyTemplate(template:String, data:Object):String
		{
			var tmpl:StringTemplate = new StringTemplate(template);
			return tmpl.apply(data);
		}
		
		
		public function apply(data:Object):String
		{
			var output:String = "";
			var n:int = templateTokens.length;
			for(var i:int=0; i<n; i++){
				output += ITemplateFragment(templateTokens[i]).exec(data);
			}
			return output;
		}
		
		private function parseTemplate(input:String):void
		{
			if(input == null){
				throw new ArgumentError("Argument cannot be null.");
			}
			templateTokens = new Array();
			var n:int = input.length;
			var inVar:Boolean = false;
			var ch:String;
			var buffer:String = "";
			var prev:String = "";
			for(var i:int=0; i<n; i++){
				ch = input.charAt(i);
				if(ch == "{" && prev != "\\"){
					inVar = true;
					templateTokens.push(new TemplateLiteral(buffer));
					buffer = "";
				}else if(ch == "}" && prev != "\\"){
					inVar = false;
					templateTokens.push(new TemplateVar(buffer, this));
					buffer = ""
				}else{
					buffer += ch;
				}
				prev = ch;
			}
			if(buffer.length > 0){
				templateTokens.push(new TemplateLiteral(buffer));
			}
		}
		
		private function getVarNames():Array
		{
			var names:Array = new Array();
			var n:int = templateTokens.length;
			var tok:TemplateVar;
			for(var i:int=0; i<n; i++){
				if((tok = templateTokens[i] as TemplateVar)){
					names.push(tok.varName);
				}
			}
			return names;
		}
	}
}


import memorphic.utils.StringTemplate;
import memorphic.utils.TemplateVarRefError;


class TemplateVar implements ITemplateFragment
{
	
	public var varName:String;
	private var propPath:Array;
	private var tmpl:StringTemplate;

	
	public function TemplateVar(varName:String, tmpl:StringTemplate)
	{
		this.varName = varName;
		propPath = varName.split(".");
		this.tmpl = tmpl;
	}
	
	public function exec(data:Object):String
	{
		var result:String;
		try
		{
			// result = data[varName];
			result = evaluateProp(data);
		}
		catch(e:ReferenceError)
		{
			if(tmpl.strict){
				throw new TemplateVarRefError(varName, e.message);
			}
		}
		// if the variable doesn't exist then just leave the string as it was 
		if(result == null){
			result = "{"+varName+"}";
		}else if(tmpl.urlEncodeParams){
			result = encodeURIComponent(result);
		}
		return result;
	}
	
	
	private function evaluateProp(data:Object):String
	{
		var v:* = data;
		var n:int = propPath.length;
		for(var i:int=0; i<n; i++){
			if(v == null){
				break;
			}
			v = v[propPath[i]];
		}
		if(v === undefined){
			throw new ReferenceError();
		}
		return v as String;
	}
	
	
}



class TemplateLiteral implements ITemplateFragment
{
	
	
	public var literal:String;
	
	public function TemplateLiteral(literal:String)
	{
		this.literal = literal;
	}
	
	public function exec(data:Object):String
	{
		return literal;
	}
}

interface ITemplateFragment
{
	function exec(data:Object):String;
}